/**
 * The controller for levels in a game
 * 
 * created 11-18
 * @author Jonathan Odom
 * @author Congyi Wu
 */

package vooga.core;

import java.awt.Graphics2D;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import vooga.entities.Entity;

public abstract class AbstractLevel{
	private Game myGame;
	private VoogaView myView;
	private Player myPlayer;
	
	private java.util.Timer myClock;
	private Object myLock = new Object();
	private int myFrameRate;
	private StepTask myStepTask;
	private vooga.entities.EntitySet myEntities;
	private Set<Entity> myEntitiesToAdd;
	private Set<Entity> myEntitiesToRemove;

	private static final int ONE_SECOND = 1000;
	private static final int DEFAULT_FRAMES_PER_SECOND = 60;
	
	private List<KeyListener> myKeyListeners;
	private List<MouseListener> myMouseListeners;
	private List<MouseMotionListener> myMouseMotionListeners;
		
	public AbstractLevel(VoogaView view, Player player, java.awt.geom.Rectangle2D abstractLevelBounds)
	{
		this(view, player, abstractLevelBounds, DEFAULT_FRAMES_PER_SECOND);
	}
	
	public AbstractLevel(VoogaView view, Player player, java.awt.geom.Rectangle2D abstractLevelBounds, int framesPerSecond)
	{
		myView = view;
		myPlayer = player;

		myKeyListeners = new LinkedList<KeyListener>();
		myMouseListeners = new LinkedList<MouseListener>();
		myMouseMotionListeners = new LinkedList<MouseMotionListener>();
		/*
		 * use a hashset for quick lookup, Java's default hash function
		 * should be fine because two entities should be different
		 * even if all their details are the same
		 */
		myEntities = new vooga.entities.EntitySet();
		myEntitiesToAdd = new HashSet<Entity>();
		myEntitiesToRemove = new HashSet<Entity>();
		
		myView.getLayerSet().add(new Layer(Layer.DEFAULT_Z_ORDER, abstractLevelBounds));

		setFrameRate(framesPerSecond);
	}
	
	
	private class StepTask extends java.util.TimerTask
	{
		AbstractLevel myParentLevel;
		public StepTask(AbstractLevel parentLevel)
		{
			myParentLevel = parentLevel;
		}
		
		/**
		 * called by timer, do all logic stuff here, override if desired
		 * by default, steps through all entities' Behaviors, then paints them
		 */
		public void run()
		{
			synchronized (myLock)
			{	
			//add all entities that were added through addEntities()
			for(Entity e : myEntitiesToAdd)
				myEntities.add(e);
			myEntitiesToAdd.clear();

			//remove all entities that were removed through removeEntities()
			for(Entity e : myEntitiesToRemove)
				myEntities.remove(e);
			myEntitiesToRemove.clear();
			
			//keep track of the Graphics we draw on so we don't have to recreate them every time;
			java.util.HashMap<Integer, Graphics2D> tempGraphics = new java.util.HashMap<Integer, Graphics2D>();
			//g.clearRect((int)f.getAbstractLeft(), (int)f.getAbstractTop(), (int)f.getAbstractWidth(), (int)f.getAbstractHeight());
			
			for(Entity e : myEntities)
			{
				if(e!=null)
				{
					e.act();
					int zOrder = e.getZOrder();
					if(!tempGraphics.containsKey(zOrder))
					{
						Layer l = myView.myLayers.get(zOrder);
						l.clear();
						tempGraphics.put(zOrder, l.getGraphics());
					}
					e.paint(tempGraphics.get(zOrder));
				}
			}
			
			//free memory
			for(Graphics2D g : tempGraphics.values())
				g.dispose();
			
			myView.getLayerSet().repaint();
			}
			checkGameOver();
			checkLevelOver();
		}
	}
	
	/**
	 * post constructor initialization that is performed just before starting
	 * the level (in other words, after the level factory has had a chance to add
	 * the entities to the level)
	 *
	 */
	public void init()
	{
		
	}
	
	/**
	 * starts game play
	 */
	final public void start(Game game)
	{
		init();
		myGame = game;
		myClock = new java.util.Timer();
		setPaused(false);
	}
	
	/**
	 * checks to see if the level has finished
	 */
	protected abstract void checkLevelOver();
	
	/**
	 * checks to see if the Game is over
	 */
	protected abstract void checkGameOver();
	
	/**
	 * pauses or unpauses game play
	 */
	final public void setPaused(boolean pause)
	{
		if(pause && myStepTask != null)
		{
			myStepTask.cancel();
			myStepTask = null;
			unregisterListeners();
		}
		else if(myStepTask == null)
		{
			myStepTask = new StepTask(this);
			myClock.schedule(myStepTask, 0L, ONE_SECOND / getFrameRate());
			registerListeners();
		}
	}
	
	final public boolean isPaused()
	{
		return myStepTask == null;
	}
	
	/**
	 * 
	 * @return true if level was started, whether it is paused or not
	 */
	final public boolean isRunning()
	{
		return myClock != null;
	}
		
	/**
	 * end level and notify game, game will end up
	 * restarting the level by default
	 */
	final public void endLevel()
	{
		synchronized (myLock) {
		if(isRunning()) {
			unregisterListeners();
			myClock.cancel();			
			myClock = null;
			myGame.nextLevel(myPlayer);
			
		}
		}
	}

	/*
	 * increment the level number, end level, and notify game
	 */
	public void nextLevel()
	{
		myPlayer.getStatus().addLevelNumber(1);
		endLevel();
	}

	/*
	 * restart the level, just call endLevel
	 */
	public void restartLevel()
	{
		endLevel();
	}

	/*
	 * set state to QUIT before ending level to end game completely
	 */
	public void quitLevel()
	{
		myPlayer.getStatus().setGameState(Status.GameState.QUIT);
		endLevel();
	}

	/*
	 * set state to GAMEOVER before ending level to notify of game over
	 */
	public void gameOver()
	{
		myPlayer.getStatus().setGameState(Status.GameState.GAME_OVER);
		endLevel();
	}

	/**
	 * add an event listener (and register it if level is running)
	 * @param l the event listener to add
	 */
	final protected void addKeyListener(KeyListener l)
	{
		myKeyListeners.add(l);
		if(!isPaused())
			myView.addKeyListener(l);
	}
	
	/**
	 * add an event listener (and register it if level is running)
	 * @param l the event listener to add
	 */
	final protected void addMouseListener(MouseListener l)
	{
		myMouseListeners.add(l);
		if(!isPaused())
			myView.addMouseListener(l);
	}

	/**
	 * add an event listener (and register it if level is running)
	 * @param l the event listener to add
	 */
	final protected void addMouseMotionListener(MouseMotionListener l)
	{
		myMouseMotionListeners.add(l);
		if(!isPaused())
			myView.addMouseMotionListener(l);
	}

	/**
	 * remove an event listener completely and disable it
	 * @param l the event listener to remove
	 */
	final protected void removeKeyListener(KeyListener l)
	{
		myKeyListeners.remove(l);
		myView.removeKeyListener(l);
	}
	
	/**
	 * remove an event listener completely and disable it
	 * @param l the event listener to remove
	 */
	final protected void removeMouseListener(MouseListener l)
	{
		myMouseListeners.remove(l);
		myView.removeMouseListener(l);
	}

	/**
	 * remove an event listener completely and disable it
	 * @param l the event listener to remove
	 */
	final protected void removeMouseMotionListener(MouseMotionListener l)
	{
		myMouseMotionListeners.remove(l);
		myView.removeMouseMotionListener(l);
	}
	
	/**
	 * 
	 * @return collection of players involved in this level
	 */
	protected Player getPlayer()
	{
		return myPlayer;
	}
		
	/**
	 * helper method to register event listeners from view (i.e. when game
	 * is started or unpaused)
	 *
	 */
	final private void registerListeners()
	{
		for(KeyListener l : myKeyListeners) myView.addKeyListener(l);
		for(MouseListener l : myMouseListeners) myView.addMouseListener(l);
		for(MouseMotionListener l : myMouseMotionListeners) myView.addMouseMotionListener(l);
	}
	
	/**
	 * helper method to unregister event listeners from view (i.e. when game
	 * is paused)
	 *
	 */
	final private void unregisterListeners()
	{
		for(KeyListener l : myKeyListeners) myView.removeKeyListener(l);
		for(MouseListener l : myMouseListeners) myView.removeMouseListener(l);
		for(MouseMotionListener l : myMouseMotionListeners) myView.removeMouseMotionListener(l);
	}
	
	/**
	 * add the entities now if paused, at beginning of next step if no paused
	 * @param entities
	 */
	public void addEntities(Collection<? extends Entity> entities)
	{
		if(isPaused())
			synchronized (myEntities)
			{
				myEntities.addAll(entities);				
			}
		else
			myEntitiesToAdd.addAll(entities);
	}

	/**
	 * add the entity now if paused, at beginning of next step if no paused
	 * @param entity
	 */
	public void addEntity(Entity e)
	{
		if(isPaused())
		{
			synchronized (myEntities)
			{
				myEntities.add(e);				
			}
		}
		else
			myEntitiesToAdd.add(e);
	}
	
	public Collection<Entity> getEntities(String group)
	{
		return myEntities.get(group);
	}
	
	public Entity getFirstEntity(String group)
	{
		return myEntities.getFirst(group);
	}	

	/**
	 * remove now if paused, next clock tick if not paused
	 * @param e
	 */
	public void removeEntity(Entity e)
	{
		if(isPaused())
			synchronized (myEntities)
			{
				myEntities.remove(e);				
			}
		else
			myEntitiesToRemove.add(e);
	}
	
	/**
	 * add a Layer to the level
	 * @param l
	 */
	final public void addLayer(Layer l)
	{
		myView.getLayerSet().add(l);
	}
	
	/**
	 * get a Layer from the level
	 * @param zOrder
	 */
	final public Layer getLayer(int zOrder)
	{
		return myView.getLayerSet().get(zOrder);
	}

	/**
	 * remove a layer from the level
	 * @param l
	 */
	final public void removeLayer(Layer l)
	{
		myView.getLayerSet().remove(l);
	}

	/**
	 * remove a layer from the level
	 * @param zOrder
	 */
	final public void removeLayer(int zOrder)
	{
		myView.getLayerSet().remove(zOrder);
	}
	
	/**
	 * set the physical size of the level on the screen
	 * @param size
	 */
	final public void setPhysicalSize(java.awt.Dimension size)
	{
		myView.setSize(size);
	}

	/**
	 * set the physical size of the level on the screen
	 * @param size
	 */
	final public void setPhysicalSize(int width, int height)
	{
		myView.setSize(width, height);
	}
	final public java.awt.Dimension getPhysicalSize()
	{
		return myView.getSize();
	}
	
	/**
	 * called when collisions are detected, should be overridden if a level
	 * would like to do something upon a collision
	 * @param entity1
	 * @param entity2
	 */
	public void handleCollision(Entity entity1, Entity entity2)
	{
	}

	//get the framerate
	public int getFrameRate() {
		return myFrameRate;
	}

	/**
	 * change the frame rate this level runs at
	 * @param framesPerSec the framerate to run at
	 */
	final protected void setFrameRate(int framesPerSec)
	{
		myFrameRate = framesPerSec;
		//update the timer if running
		if(!isPaused())
		{
			setPaused(true);
			setPaused(false);
		}
	}
}
